package br.edu.ifrn.potigol.lang;

import java.io.*;
import java.util.Scanner;

import br.edu.ifrn.potigol.lang.ArgumentError;
import br.edu.ifrn.potigol.lang.Context;
import br.edu.ifrn.potigol.lang.Method;
import br.edu.ifrn.potigol.lang.OperatorMethod;
import br.edu.ifrn.potigol.lang.PotigolClass;
import br.edu.ifrn.potigol.lang.PotigolException;
import br.edu.ifrn.potigol.lang.PotigolObject;
import br.edu.ifrn.potigol.lang.PotigolRuntime;
import br.edu.ifrn.potigol.lang.ValueObject;


/**
 * Responsável pela criação das classes principais e adição de métodos.
 */
public class Bootstrapper {
	/**
	 * Chamado para inicializar o runtime
	 * @return Contexto	raiz
	 */
	static public Context run() {
		//Cria classes principais
	    PotigolClass objectClass = new PotigolClass("Object");
	    PotigolRuntime.objectClass = objectClass;
	    
	    //Cada método enviado ou adicionado no contexto de raiz de um script são avaliados sobre o objeto principal.
	    PotigolObject main = new PotigolObject();
	    PotigolRuntime.mainObject = main;
	    PotigolClass classClass = new PotigolClass("Class");
	    objectClass.setPotigolClass(classClass); //Objeto é uma classe
	    classClass.setPotigolClass(classClass); //Classe é uma classe
	    main.setPotigolClass(objectClass);
	    
	    //Registra-se as classes principais no contexto raiz
	    objectClass.setConstant("Object", objectClass);
	    objectClass.setConstant("Class", classClass);

	    //Existe apenas uma instância de nil, true, false, por isso esses dados serão armazenados em constantes.
	    PotigolRuntime.nilObject = objectClass.newSubclass("NilClass").newInstance(null);
	    PotigolRuntime.trueObject = objectClass.newSubclass("TrueClass").newInstance(true);
	    PotigolRuntime.falseObject = objectClass.newSubclass("FalseClass").newInstance(false);
	    PotigolClass stringClass = objectClass.newSubclass("String");
	    PotigolClass numberClass = objectClass.newSubclass("Number");
	    @SuppressWarnings("unused")
	    PotigolClass integerClass = numberClass.newSubclass("Integer");
	    @SuppressWarnings("unused")
		PotigolClass floatClass = numberClass.newSubclass("Float");
	    PotigolClass exceptionClass = objectClass.newSubclass("Excecao");
	    exceptionClass.newSubclass("ErroIO");
	    exceptionClass.newSubclass("ErroTipo");
	    exceptionClass.newSubclass("MetodoNaoEncontrado");
	    exceptionClass.newSubclass("ErroIndice");
	    exceptionClass.newSubclass("ErroArgumento");
	    exceptionClass.newSubclass("ArquivoNaoEncontrado");
	    exceptionClass.newSubclass("Erro");
	    //Adiciona métodos para as classes principais.
	   
	    //Objeto
	    objectClass.addMethod("imprima", new Method() {
	      public PotigolObject call(PotigolObject receiver, PotigolObject arguments[]) throws PotigolException {
	        for (PotigolObject arg : arguments) System.out.println(arg.toJavaObject());
	        return PotigolRuntime.getNil();
	      }
	    });
	    objectClass.addMethod("leia", new Method() {
	      public PotigolObject call(PotigolObject receiver, PotigolObject arguments[]) throws PotigolException {
		    Scanner sc = new Scanner(System.in); 
		    String input = sc.nextLine();
		    return new ValueObject(input);
		  }
		});
	    objectClass.addMethod("classe", new Method() {
	      public PotigolObject call(PotigolObject receiver, PotigolObject arguments[]) throws PotigolException {
	        return receiver.getPotigolClass();
	      }
	    });
	    objectClass.addMethod("avalie", new Method() {
	      public PotigolObject call(PotigolObject receiver, PotigolObject arguments[]) throws PotigolException {
	        Context context = new Context(receiver);
	        String code = arguments[0].asString();
	        return context.eval(code);
	      }
	    });
	    objectClass.addMethod("importe", new Method() {
	      public PotigolObject call(PotigolObject receiver, PotigolObject arguments[]) throws PotigolException {
	        Context context = new Context();
	        String filename = arguments[0].asString();
	        try {
	          return context.eval(new FileReader(filename));
	        } catch (FileNotFoundException e) {
	          throw new PotigolException("ArquivoNaoEncontrado", "Arquivo nao encontrado: " + filename);
	        }
	      }
	    });
	    objectClass.addMethod("numero?", new Method() {
			public PotigolObject call(PotigolObject receiver, PotigolObject[] arguments) throws PotigolException {
				if (receiver.toJavaObject() instanceof Integer || receiver.toJavaObject() instanceof Float) {
	    			return PotigolRuntime.getTrue();	    			
	    		}
	    		return PotigolRuntime.getFalse();
			}
		}); 
	    
	    //Classe
	    classClass.addMethod("novo", new Method() {
	      public PotigolObject call(PotigolObject receiver, PotigolObject arguments[]) throws PotigolException {
	        PotigolClass self = (PotigolClass) receiver;
	        PotigolObject instance = self.newInstance();
	        if (self.hasMethod("inicializar")) instance.call("inicializar", arguments);
	        return instance;
	      }
	    });
	    classClass.addMethod("nome", new Method() {
	      public PotigolObject call(PotigolObject receiver, PotigolObject arguments[]) throws PotigolException {
	        PotigolClass self = (PotigolClass) receiver;
	        return new ValueObject(self.getName());
	      }
	    });
	    classClass.addMethod("superclasse", new Method() {
	      public PotigolObject call(PotigolObject receiver, PotigolObject arguments[]) throws PotigolException {
	        PotigolClass self = (PotigolClass) receiver;
	        return self.getSuperClass();
	      }
	    });
	
	    //Exceção
	    exceptionClass.addMethod("inicializar", new Method() {
	      public PotigolObject call(PotigolObject receiver, PotigolObject arguments[]) throws PotigolException {
	        if (arguments.length == 1) receiver.setInstanceVariable("mensagem", arguments[0]);
	        return PotigolRuntime.getNil();
	      }
	    });
	    exceptionClass.addMethod("mensagem", new Method() {
	      public PotigolObject call(PotigolObject receiver, PotigolObject arguments[]) throws PotigolException {
	        return receiver.getInstanceVariable("mensagem");
	      }
	    });
	    objectClass.addMethod("provocar", new Method() {
	      public PotigolObject call(PotigolObject receiver, PotigolObject arguments[]) throws PotigolException {
	        String message = null;
	        if (receiver.hasInstanceVariable("mensagem")) message = receiver.getInstanceVariable("mensagem").asString();
	        throw new PotigolException(receiver.getPotigolClass(), message);
	      }
	    });
	    
	    //Número
	    numberClass.addMethod("+", new OperatorMethod<Number>() {
	    	public PotigolObject perform(Number receiver, Number argument) throws PotigolException {
	    		if (receiver instanceof Integer && argument instanceof Integer)
	    			return new ValueObject(receiver.intValue() + argument.intValue());
	    		if (receiver instanceof Float || argument instanceof Float)
	    			return new ValueObject(receiver.floatValue() + argument.floatValue());
	    		
	    		return null;
	    	}
	    });
	    numberClass.addMethod("-", new OperatorMethod<Number>() {
	    	public PotigolObject perform(Number receiver, Number argument) throws PotigolException {
	    		if (receiver instanceof Integer && argument instanceof Integer)
	    			return new ValueObject(receiver.intValue() - argument.intValue());
	    		if (receiver instanceof Float || argument instanceof Float)
	    			return new ValueObject(receiver.floatValue() - argument.floatValue());
	    		
	    		return null;
	    	}
	    });
	    numberClass.addMethod("*", new OperatorMethod<Number>() {
	    	public PotigolObject perform(Number receiver, Number argument) throws PotigolException {
	    		if (receiver instanceof Integer && argument instanceof Integer)
	    			return new ValueObject(receiver.intValue() * argument.intValue());
	    		if (receiver instanceof Float || argument instanceof Float)
	    			return new ValueObject(receiver.floatValue() * argument.floatValue());
	    		
	    		return null;
		    }
	    });
	    numberClass.addMethod("/", new OperatorMethod<Number>() {
			public PotigolObject perform(Number receiver, Number argument) throws PotigolException {
				if (receiver instanceof Integer && argument instanceof Integer)
	    			return new ValueObject(receiver.intValue() / argument.intValue());
	    		if (receiver instanceof Float || argument instanceof Float)
	    			return new ValueObject(receiver.floatValue() / argument.floatValue());
	    		
	    		return null;
			}
		});
	    numberClass.addMethod("%", new OperatorMethod<Number>() {
			public PotigolObject perform(Number receiver, Number argument) throws PotigolException {
				if (receiver instanceof Integer && argument instanceof Integer)
	    			return new ValueObject(receiver.intValue() % argument.intValue());
	    		if (receiver instanceof Float || argument instanceof Float)
	    			return new ValueObject(receiver.floatValue() % argument.floatValue());
	    		
	    		return null;
			}
		});
	    numberClass.addMethod("<", new OperatorMethod<Number>() {
			public PotigolObject perform(Number receiver, Number argument) throws PotigolException {
				if (receiver instanceof Integer && argument instanceof Integer)
	    			return PotigolRuntime.toBoolean(receiver.intValue() < argument.intValue());
	    		if (receiver instanceof Float || argument instanceof Float)
	    			return PotigolRuntime.toBoolean(receiver.floatValue() < argument.floatValue());
	    		
	    		return null;
			}
		});
	    numberClass.addMethod(">", new OperatorMethod<Number>() {
			public PotigolObject perform(Number receiver, Number argument) throws PotigolException {
				if (receiver instanceof Integer && argument instanceof Integer)
	    			return PotigolRuntime.toBoolean(receiver.intValue() > argument.intValue());
	    		if (receiver instanceof Float || argument instanceof Float)
	    			return PotigolRuntime.toBoolean(receiver.floatValue() > argument.floatValue());
	    		
	    		return null;
			}
		});
	    numberClass.addMethod("<=", new OperatorMethod<Number>() {
			public PotigolObject perform(Number receiver, Number argument) throws PotigolException {
				if (receiver instanceof Integer && argument instanceof Integer)
	    			return PotigolRuntime.toBoolean(receiver.intValue() <= argument.intValue());
	    		if (receiver instanceof Float || argument instanceof Float)
	    			return PotigolRuntime.toBoolean(receiver.floatValue() <= argument.floatValue());
	    		
	    		return null;
			}
		});
	    numberClass.addMethod(">=", new OperatorMethod<Number>() {
			public PotigolObject perform(Number receiver, Number argument) throws PotigolException {
				if (receiver instanceof Integer && argument instanceof Integer)
	    			return PotigolRuntime.toBoolean(receiver.intValue() >= argument.intValue());
	    		if (receiver instanceof Float || argument instanceof Float)
	    			return PotigolRuntime.toBoolean(receiver.floatValue() >= argument.floatValue());
	    		
	    		return null;
			}
		});
	    numberClass.addMethod("==", new OperatorMethod<Number>() {
			public PotigolObject perform(Number receiver, Number argument) throws PotigolException {
				if (receiver instanceof Integer && argument instanceof Integer)
	    			return PotigolRuntime.toBoolean(receiver.intValue() == argument.intValue());
	    		if (receiver instanceof Float || argument instanceof Float)
	    			return PotigolRuntime.toBoolean(receiver.floatValue() == argument.floatValue());
	    		
	    		return null;
			}
		});
	    
	    //Texto
	    stringClass.addMethod("+", new OperatorMethod<String>() {
	      public PotigolObject perform(String receiver, String argument) throws PotigolException {
	        return new ValueObject(receiver + argument);
	      }
	    });
	    stringClass.addMethod("tamanho", new Method() {
	      public PotigolObject call(PotigolObject receiver, PotigolObject arguments[]) throws PotigolException {
	        String self = receiver.asString();
	        return new ValueObject(self.length());
	      }
	    });
	    stringClass.addMethod("subtexto", new Method() {
	      public PotigolObject call(PotigolObject receiver, PotigolObject arguments[]) throws PotigolException {
	        String self = receiver.asString();
	        if (arguments.length == 0) throw new ArgumentError("subtexto", 1, 0);
	        int start = arguments[0].asInteger();
	        int end = self.length();
	        if (arguments.length > 1) end = arguments[1].asInteger();
	        return new ValueObject(self.substring(start, end));
	      }
	    });
	    stringClass.addMethod("int", new Method() {
	    	public PotigolObject call(PotigolObject receiver, PotigolObject arguments[]) throws PotigolException {
				return new ValueObject(Integer.valueOf(receiver.asString()));
	    	}
		});
	    
	    //Arranjo
	    objectClass.addMethod("tamanho", new Method() {
	    	public PotigolObject call(PotigolObject receiver, PotigolObject arguments[]) throws PotigolException {
	    		PotigolArrayObject self = (PotigolArrayObject) receiver;
		        return new ValueObject(self.getArray().length);
	    	}
		});
	    
	    //Retorna o contexto raiz em que tudo será avaliado. Por padrão, tudo é avaliado sobre o objeto principal.
	    return new Context(main);
	}
}
